<?php

namespace s94\wechat;

use Exception;

/**
 * 网页开发
 */
class Wap extends Base
{

    private $apiList = [
        [//0分享
            'updateAppMessageShareData',//自定义“分享给朋友”及“分享到QQ”按钮的分享内容（1.4.0）
            'updateTimelineShareData',	//自定义“分享到朋友圈”及“分享到QQ空间”按钮的分享内容（1.4.0）
            'onMenuShareWeibo',			//获取“分享到腾讯微博”按钮点击状态及自定义分享内容接口
        ],[//1语音
            'startRecord',				//开始录音接口
            'stopRecord',				//停止录音接口
            'onVoiceRecordEnd',			//监听录音自动停止接口
            'playVoice',				//播放语音接口
            'pauseVoice',				//暂停播放接口
            'stopVoice',				//停止播放接口
            'onVoicePlayEnd',			//监听语音播放完毕接口
            'uploadVoice',				//上传语音接口
            'downloadVoice',			//下载语音接口
        ],[//2图像
            'chooseImage',				//拍照或从手机相册中选图接口
            'previewImage',				//预览图片接口
            'uploadImage',				//上传图片接口
            'downloadImage',			//下载图片接口
            'getLocalImgData',			//获取本地图片接口
        ],[//3界面
            'hideOptionMenu',
            'showOptionMenu',
            'hideMenuItems',			//批量隐藏功能按钮接口
            'showMenuItems',			//批量显示功能按钮接口
            'hideAllNonBaseMenuItem',	//隐藏所有非基础按钮接口
            'showAllNonBaseMenuItem',	//显示所有功能按钮接口
            'closeWindow',				//关闭当前网页窗口接口
        ],[//4微信支付
            'chooseWXPay',				//发起一个微信支付请求
        ],[//5语音识别
            'translateVoice',			//识别音频并返回识别结果接口
        ],[//6地理位置
            'openLocation',				//使用微信内置地图查看位置接口
            'getLocation',				//获取地理位置接口
        ],[//7网络状态
            'getNetworkType',			//获取网络状态接口
        ],[//8扫一扫
            'scanQRCode',				//调起微信扫一扫接口
        ],[//9微信小店
            'openProductSpecificView',	//跳转微信商品页接口
        ],[//10微信卡券
            'addCard',					//批量添加卡券接口
            'chooseCard',				//拉取适用卡券列表并获取用户选择信息
            'openCard',					//查看微信卡包中的卡券接口
        ],[//11摇一摇周边
            'startSearchBeacons',		//开启查找周边ibeacon设备接口
            'stopSearchBeacons',		//关闭查找周边ibeacon设备接口
            'onSearchBeacons',			//监听周边ibeacon设备接口
        ],[//12快速输入
            'openAddress'				//共享收货地址接口
        ]
    ];
    private $openTagList = [
        'wx-open-launch-weapp', //跳转小程序
        'wx-open-launch-app', //跳转App
        'wx-open-subscribe', //服务号订阅通知
        'wx-open-audio', //音频播放
    ];

    protected function jsapiTicket($refresh=false)
    {
        $cache_key = 'jsapiTicket_'.$this->config('appid');
        $jsapi_ticket = $this->cache($cache_key);
        if (!$jsapi_ticket || $refresh){//过期或者强制刷新
            $param = [
                'access_token'=> $this->accessToken(),
                'type'=>'jsapi',
            ];
            $res = $this->apiSdk('cgi-bin/ticket/getticket',$param);
            $jsapi_ticket = $res['ticket'];
            $this->cache([$cache_key=>$jsapi_ticket], 7000);
        }
        return $jsapi_ticket;
    }

    /**获取指定版本的微信js
     * @param string $version 版本字符串，例如：1.6.0
     * @return false|mixed|string
     * @throws Exception
     */
    private function jweixinJs($version)
    {
        $path = $this->config('cache_dir').'jweixin-'.$version.'.js';
        $url = 'http://res.wx.qq.com/open/js/jweixin-'.$version.'.js';
        if (is_file($path)) {
            return file_get_contents($path);
        }else{
            $res = self::curl($url);
            self::assert($res['code']==200,"SDK版本【{$version}】错误");
            file_put_contents($path, $res['body']);
            return $res['body'];
        }
    }

    /**默认的来源地址，优先级为：1、请求的$_GET['referer']，2、$_SERVER['HTTP_REFERER']
     * @param $remove_hash
     * @return array|mixed|string|string[]
     */
    public static function defaultReferer($remove_hash=false)
    {
        $res = isset($_GET['referer']) ? urldecode($_GET['referer']) : ($_SERVER['HTTP_REFERER'] ?? '');
        if ($remove_hash) $res = str_replace("/\#.*$/",'', $res);
        return $res;
    }

    private static function urlMergeParam($url, $param)
    {
        $url_arr = parse_url($url);
        $query_str = empty($url_arr['query']) ? '' : $url_arr['query'];
        $query = [];
        if ($query_str) parse_str($query_str, $query);
        $query = array_merge($query, $param);
        // 移除null
        foreach ($query as $k=>$v){
            if ($v===null) unset($query[$k]);
        }
        $query_str_new = http_build_query($query);

        //去掉旧的参数和hash
        $fragment = empty($url_arr['fragment']) ? '' : $url_arr['fragment'];
        $old = '';
        if ($query_str) $old .= '?'.$query_str;
        if ($fragment) $old .= '#'.$fragment;
        $url = substr($url, 0, strlen($url)-strlen($old));
        $url = rtrim($url, '?');
        //拼接新的参数和hash
        if ($query_str_new) $url .= '?'.$query_str_new;
        if ($fragment) $url .= '#'.$fragment;
        return $url;
    }

    /**JS-SDK的配置参数
     * @param mixed $need_api 需要的接口序号，用','分隔【null为所有接口】
     * 0分享|1音频|2图像|3界面|4支付|5语音识别|6位置|7网络状态|8扫一扫|9小店|10卡卷|11摇一摇|12快速输入
     * @return array
     * @throws Exception
     */
    public function sdkConfig($need_api=null, $referer=null)
    {
        $time = time();
        $nonce_str = self::randomStr();
        if (!$referer) $referer = self::defaultReferer(true);
        //生成签名
        $sign_arr = [
            'jsapi_ticket'=>$this->jsapiTicket(),
            'noncestr'=>$nonce_str,
            'timestamp'=>$time,
            'url'=>$referer,
        ];
        $buff='';
        foreach ($sign_arr as $k=>$v){
            $buff .= $k."=".$v."&";
        }
        $sign = sha1(substr($buff,0,-1));
        $list_api = [];
        $need_api = $need_api ? explode(',', $need_api) : array_keys($this->apiList);
        foreach ($need_api as $i){
            if (empty($this->apiList[$i])) continue;
            $list_api = array_merge($list_api,$this->apiList[$i]);
        }
        return [
            'appId'=> $this->config('appid'),
            'timestamp'=> $time,
            'nonceStr'=> $nonce_str,
            'signature'=> $sign,
            'jsApiList'=> $list_api,
            'openTagList'=> $this->openTagList,
        ];
    }

    /**返回js-sdk的js代码
     * @param mixed $debug 是否开启debug
     * @param string $version sdk版本，默认为：1.6.0
     * @param mixed $is_es6 是否为es6模块方式
     * @param mixed $need_api 需要的接口序号，用','分隔【null为所有接口】
     * 0分享|1音频|2图像|3界面|4支付|5语音识别|6位置|7网络状态|8扫一扫|9小店|10卡卷|11摇一摇|12快速输入
     * @return string
     */
    public function sdkCode($debug=false, string $version='1.6.0', $is_es6=false, $need_api=null, $referer=null): string
    {
        $js = $this->jweixinJs($version);
        try {
            $config = $this->sdkConfig($need_api, $referer);
            if ($debug) $config['debug'] = true;
            $js .= 'wx.config('.json_encode($config, JSON_UNESCAPED_UNICODE).');';
        } catch (Exception $e) {
            $js = "var wx=new Proxy({},{get(obj, prop){alert('微信jssdk配置出错：".($debug?$e->getMessage():"")."');}});";
        }
        if ($is_es6){
            return str_replace('(this,','(globalThis,',$js)."export default wx;";
        }else{
            return '(function(factory) {"use strict";if (typeof require === "function" && typeof module === "object" && typeof module.exports === "object"){module.exports = factory();} else {window["wx"] = factory();}})(function(){' . $js . 'return wx;})';
        }
    }
    public function sdkJs($debug=false, string $version='1.6.0', $is_es6=false, $need_api=null, $referer=null)
    {
        header('Content-Type:application/javascript; charset=utf-8');
        exit($this->sdkCode($debug, $version, $is_es6, $need_api, $referer));
    }

    /**api接口授权方式，支持静态调用，无需配置appid、appsecret等参数
     * @param string $api 授权接口地址，该接口的服务器获取到微信授权信息后，跳转到referer参数的地址，并且把用户数据和appid作为查询参数附加到地址后面
     * @param string $referer 引入页地址，也就是获取用户信息后跳回的地址，如果为空，会取当前请求的$_GET['referer']或者$_SERVER['HTTP_REFERER']
     * @param mixed $auto 是否自动重定向|false
     * @return string 重定向地址(string)
     * @throws Exception
     */
    public static function userInfoApi($api, $referer=null, $auto=false): string
    {
        self::assert($api, '授权接口地址不能为空。');
        $base = $_GET['base'] ?? '';
        if (!$referer) $referer = self::defaultReferer();
        //api模式，通过跳转到api接口地址，获取用户信息后，再跳转回来，同时吧用户信息附加到GET里
        $param = ['referer'=>$referer, 'base'=>$base];
        $url = self::urlMergeParam($api, $param);
        if (!$auto) return $url;
        Header('Location: '.$url);
        exit();
    }

    /**微信浏览器授权，获取的用户信息会以url参数的形式附加到referer地址后面，如果只需要openid可以通过传入$_GET['base']=1实现；
     * 注意：调用此方法的接口不要带code、state参数，会和微信授权参数冲突
     * @param mixed $auto 是否自动重定向，默认(false)返回重定向地址
     * @param string $referer 来源地址，用户授权后，跳回的地址，默认为：self::defaultReferer()
     * @param mixed $ignore_code 是否跳过当前code参数，如果为true即使存在$_GET['code']也会先跳转到微信授权页面
     * @return string 需要重定向的地址
     * @throws Exception
     */
    public function userInfo($auto=false, $referer=null, $ignore_code=false): string
    {
        if (!$referer) $referer = self::defaultReferer();
        $code = $_GET['code'] ?? '';
        $state = $_GET['state'] ?? '';
        if (!$code || $state != 's940WechatWebAuthorize' || $ignore_code){
            //触发微信返回code码
            $base = $_GET['base'] ?? '';
            $http_type = ((isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] == 'on') || (isset($_SERVER['SERVER_PORT']) && $_SERVER['SERVER_PORT'] == '443')) ? 'https://' : 'http://';
            $host = isset($_SERVER['HTTP_HOST']) ? preg_replace("/:\d+$/",'',$_SERVER['HTTP_HOST']) : '';
            $back_url = $http_type.$host.$_SERVER['REQUEST_URI']; //当前url作为微信回调地址，并且添加referer参数，去除code、state参数
            $back_url = self::urlMergeParam($back_url, ['referer'=>$referer,'code'=>null,'state'=>null]);
            $param = [
                'appid'=>$this->config('appid'),
                'redirect_uri'=>$back_url,
                'response_type'=>'code',
                'scope'=>$base ? 'snsapi_base' : 'snsapi_userinfo',
                'state'=>'s940WechatWebAuthorize',
            ];
            //请求地址重定向
            $url = 'https://open.weixin.qq.com/connect/oauth2/authorize?'.http_build_query($param).'#wechat_redirect';
        } else {
            //通过code码，以获取access_token(网页)和openid
            $param = [
                'appid'=>$this->config('appid'),
                'secret'=>$this->config('appsecret'),
                'code'=>$_GET['code'],
                'grant_type'=>'authorization_code'
            ];
            try {
                $res = $this->apiSdk('sns/oauth2/access_token', $param);
                $data = ['appid'=>$param['appid'],'openid'=>$res['openid']];
                if ($res['scope'] == 'snsapi_userinfo'){
                    $param = [
                        'access_token'=>$res['access_token'],//注意！！这里获取的access_token只能用于网页授权
                        'openid'=>$res['openid'],
                        'lang'=>'zh_CN'
                    ];
                    try {
                        //此处通过授权的 access_token 获取用户信息，有时候会报错，如果报错，就只返回基本信息
                        $res = $this->apiSdk('sns/userinfo', $param, [], true);
                        $data = array_merge($data, $res);
                    } catch (SdkException $e) {}
                }
                $url = self::urlMergeParam($referer, $data);
            } catch (SdkException $e) {
                //此处如果遇到 oauth_code已使用 等错误时，忽略当前code，重新获取code
                $url = $this->userInfo($auto, $referer, true);
            }
        }
        if (!$auto) return $url;
        Header('Location: '.$url);
        exit();
    }
}
